iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Modern Web

React 測試 x AI:探索測試新境界,測試不再枯燥乏味!系列 第 6 篇

[Day 06] React Testing Library 語法介紹

  • 分享至 

  • xImage
  •  

前面提到 Jest 本身有提供很多測試方法,但在測試上都比較偏向邏輯測試,像是 a + b 是否等於 c。而實際上我們所需要的測試有一大部分也包含 UI 的測試,像是畫面上有沒顯示正確文字,或是使用者點擊有沒有跳出視窗等等,這時候就需要使用 Testing Library 。

Testing Library 文檔介紹開頭就很明確的說了

The @testing-library family of packages helps you test UI components in a user-centric way.

也就是 @testing-library 系列的套件可以幫助你以用戶為中心的方式測試 UI 元件。

Testing Library 不只提供可以共通使用的測試套件,還有專為各個框架所出的套件,基本的三大框架 React、Vue、Angular 都有屬於他們的測試 UI 套件。而比較新的框架像是 Preact、Svelte 也都有在持續更新。

Testing Library For React

以 React 來說,搭配 Jest 會需要的 Testing Libary 有以下幾個:

  1. @testing-library/react - 模擬 React 渲染,Ex:render、screen

  2. @testing-library/jest-dom - 擴充 jest 的斷言庫,Ex:toBeInTheDocument、toHaveClass

  3. @testing-library/user-event - 模擬使用者操作,Ex:useEvent.click、userEvent.type

接下來來各別介紹這三個的使用時機:

📌 @testing-library/react

主要是提供模擬渲染 UI 畫面的函式,比較常用的有

  1. render()

就如同 React 中的 render,就是模擬渲染 React 元件,假設有一個 <Home/> 元件:

export const Home = () => {
  return <h1>Home Page</h1>;
};

要測試元件的 DOM 元素時,就可以使用 render() 函式。

import { render } from "@testing-library/react";
import { Home } from "./Home";

describe("testing home component", () => {
  it("show Home Page in the home component", () => {
    const { getByText } = render(<Home />);
    //使用 getByText 判斷抓到文字的元素是否存在
    expect(getByText("Home Page")).toBeTruthy();
  });
});

render() 函式會回傳一個物件包含一些屬性對象:

  • ...Query:getBy、queryBy、findBy、getAllBy、queryAllBy、findAllBy,取得元件內的元素。
  • container:渲染的 DOM 節點。
  • debug:偵錯函式,可以顯示當前的 DOM 結構。
  • rerender:重新渲染元件。
  • unmount:取消渲染。

Query 有很多不同的取元素方法,有 get、find、query,各自都有取得多元素的 all 方法

Query 種類\結果 沒有符合 一項符合 多項符合 是否為非同步函式
getBy... Throw Error 回傳元素 Throw Error ❌
queryBy... 回傳 null 回傳元素 Throw Error ❌
findBy... Throw Error 回傳元素 回傳元素 ✅
getAllBy... Throw Error 回傳元素陣列 回傳元素陣列 ❌
queryAllBy... 回傳 [] 回傳元素陣列 回傳元素陣列 ❌
findAllBy... Throw Error 回傳元素陣列 回傳元素陣列 ✅

看起來很容易搞混,不過他們都有各自使用的時機

  • getBy...:大部分都可以使用,判斷元素是否存在。
  • queryBy...:因為找不到會回傳 null 的特性,所以常用來判斷元素是否一開始不存在。
  • findBy...:非同步函式,可以判斷需等待的元素是否存在,例如 API 回傳才會顯示在畫面上。
  1. screen()

雖然 render() 解決了模擬 UI 的情況,不過只能根據 render() 的內容進行測試,如果有多個 render() 函式測起來就會比較麻煩,這時候就可以使用 screen()。

其實 screen() 算是 @testing-library/dom 所提供,而所有框架的 @testing-library 底層都有 @testing-library/dom,所以這邊才可以直接做使用。

而 screen() 所抓取的元素是 <body></body> 內的所有 DOM 元素。

import { render, screen } from "@testing-library/react";
import { Home } from "./Home";

describe("testing home component", () => {
  it("show Home Page in the home component", () => {
    render(<Home />);
    render(<Home />);
    //使用 getAllByText 判斷是否抓取兩個元素
    expect(screen.getAllByText("Home Page")).toHaveLength(2);
  });
});

個人在抓元素時是比較常用 screen() 勝過於使用 render() 回傳的方法,使用起來也比較方便。

  1. renderHook()

顧名思義就是去模擬客製化的 React Hook,假設今天有一個計數器的 custom hook

import { useState } from "react";

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  // 加一函式
  const increment = () => {
    setCount(count + 1);
  };
  // 減一函式
  const decrement = () => {
    setCount(count - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;

要測試這個 hook 就可以使用 renderHook(),他可以傳入兩個參數,第一個就是要測試的函式,第二是傳入函式的參數 (非必要)。

import useCounter from "./useCounter";
import { renderHook, act } from "@testing-library/react";

it("should increment and decrement the counter", () => {
  const { result } = renderHook(useCounter);

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment(); // 呼叫 + 1 函式
  });

  expect(result.current.count).toBe(1);

  act(() => {
    result.current.decrement(); // 呼叫 -1 函式
  });

  expect(result.current.count).toBe(0);
});

renderHook() 會回傳一個物件,可以使用 result 屬性利用 result.current 去操控函式的回傳物件。

如果有更改 state 的操作,就需要使用 act 包起來,這樣才可以即時更新 state 的狀態。

📌 @testing-library/jest-dom

@testing-library/jest-dom 提供給 Jest 很多 DOM 元素的擴充判斷,讓我們在抓元素時,有更多的方法去測試,比較常用的像是 toBeInTheDocument、toHaveClass 等等。

以剛剛的 <Home/> 渲染測試為例

import { render, screen } from "@testing-library/react";

describe("testing home component", () => {
  it("show Home Page in the home component", () => {
    render(<Home />);
    // 判斷元素是否存在於 DOM 中
    expect(screen.getByText("Home Page")).toBeInTheDocument(); 
  });
});

或是

import { render, screen } from "@testing-library/react";

describe("testing div", () => {
  it("test div className have 'hide'", () => {
    render(<div className='hide'>test</div>);
    //判斷元素是否含有指定的 class name
    expect(screen.getByText("test")).toHaveClass("hide"); 
  });
});

📌 @testing-library/user-event

最後一個就是模擬使用著操作,user-event 常常跟另一個 @testing-library/react 的 fireEvent 拿來比較,fireEvent 就是在程式碼中會用的事件處理,像是 click 或是 change,而 user-event 的底層就是 fireEvent,不過 userEvent 能更貼合使用者的模擬情況,像是同樣是在 input 輸入文字,如果使用 fireEvent 就會使用 change 的事件。

fireEvent:

import { fireEvent } from "@testing-library/react";

fireEvent.change(inputElement, { target: { value: "Hello World!" } });

不過如果是使用 userEvent 就會使用 type 的事件。

userEvent:

import userEvent from "@testing-library/user-event";

const user = userEvent.setup();
await user.type(inputElement, "Hello World!");

除了 userEvent 是非同步以外,乍看之下沒有什麼不一樣,不過 userEvent 的 type 還包含使用者點擊 input,輸入文字的 keyDown、keyUp 事件,比起 fireEvent 的 change,更貼近實際的使用者操作情況。

假設有一個 input,輸入文字的同時會顯示 KeyDown、KeyUp 的文字

import React, { useState } from "react";

function KeyEvent() {
  const [text, setText] = useState("");
  const [keyEvent, setKeyEvent] = useState("");

  // keyDown 事件
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    setKeyEvent((prev) => prev + `Key Down: ${e.key}`);
  };
  // keyUp 事件
  const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
    setKeyEvent((prev) => prev + `Key Up: ${e.key}`);
  };

  return (
    <div>
      <input
        type='text'
        value={text}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
          setText(e.target.value)
        }
        onKeyDown={handleKeyDown}
        onKeyUp={handleKeyUp}
        data-testid='input'
      />
      <p data-testid='key-event'>{keyEvent}</p>
    </div>
  );
}

export default KeyEvent;

使用 firEvent 來測試

describe("KeyEventsComponent", () => {
  it("test with fireEvent", async () => {
    render(<KeyEvent />);
    const inputElement = screen.getByTestId("input");
    const keyEventElement = screen.getByTestId("key-event");

    // 使用 fireEvent.change 輸入文字
    fireEvent.change(inputElement, { target: { value: "Hello" } });

    // 使用 fireEvent.keyDown 模擬使用者按下 o
    fireEvent.keyDown(inputElement, { key: "o" });

    // 使用 fireEvent.keyUp 模擬使用者放開 o
    fireEvent.keyUp(inputElement, { key: "o" });

    expect(inputElement).toHaveValue("Hello");
    expect(keyEventElement.textContent).toContain("Key Down: o");
    expect(keyEventElement.textContent).toContain("Key Up: o");
  });
});

可以看到需要把 change 事件跟 keyDown、keyUp 事件分開寫,寫起來比較麻煩外,也不符合實際的使用者輸入。

如果使用 userEvent 來測試

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import KeyEvent from "./KeyEvent";

const user = userEvent.setup();

describe("KeyEventsComponent", () => {
  it("should simulate key events with userEvent.type", async () => {
    render(<KeyEvent />);
    const inputElement = screen.getByTestId("input");
    const keyEventElement = screen.getByTestId("key-event");

    // 使用 user.type 輸入文字
    await user.type(inputElement, "Hello");

    // 判斷 input 的值是否為 Hello
    expect(inputElement).toHaveValue("Hello");

    // 判斷是否有顯示 KeyDown、KeyUp 的文字
    expect(keyEventElement.textContent).toContain("Key Down: o");
    expect(keyEventElement.textContent).toContain("Key Up: o");
  });
});

只需要一個 user.type 就可以包含所有的事件,也更貼近使用者的操作。

除此之外,userEvent 還有提供很多實用的方法像是:

  • dblClick:點擊兩次

  • tripleClick:點擊三次

  • type:input 輸入

  • upload:上傳

  • hover/unhover:指標移進移出

  • copy/paste:複製/貼上

詳細的可以參考 User Interactions

今天就介紹到這邊~下一篇終於要進入 AI 的部分了!


上一篇
[Day 05] React 測試安裝介紹 ( CRA / Vite / Next.js )
下一篇
[Day 07] 寫測試 AI 工具推薦
系列文
React 測試 x AI:探索測試新境界,測試不再枯燥乏味! 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言